// Copyright (c) 2022 Huawei Device Co., Ltd. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "oh_gin_javascript_bridge_dispatcher.h"
#include "base/auto_reset.h"
#include "cef/libcef/common/javascript/oh_gin_javascript_bridge_messages.h"
#include "cef/libcef/renderer/javascript/oh_gin_javascript_bridge_object.h"
#include "content/public/renderer/render_frame.h"

#include "third_party/blink/public/web/blink.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_view.h"

#include "libcef/common/javascript/oh_gin_javascript_bridge_value.h"
#include "libcef/renderer/javascript/oh_gin_javascript_bridge_value_converter.h"

namespace NWEB {
OhGinJavascriptBridgeDispatcher::OhGinJavascriptBridgeDispatcher(
    content::RenderFrame* render_frame)
    : content::RenderFrameObserver(render_frame),
      inside_did_clear_window_object_(false),
      converter_(new OhGinJavascriptBridgeValueConverter(AsWeakPtr())) {}

OhGinJavascriptBridgeDispatcher::~OhGinJavascriptBridgeDispatcher() {
  for (auto item : h5_object_map_) {
    item.second->Reset();
    delete (item.second);
  }
}

bool OhGinJavascriptBridgeDispatcher::OnMessageReceived(
    const IPC::Message& msg) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(OhGinJavascriptBridgeDispatcher, msg)
    IPC_MESSAGE_HANDLER(OhGinJavascriptBridgeMsg_AddNamedObject,
                        OnAddNamedObject)
    IPC_MESSAGE_HANDLER(OhGinJavascriptBridgeMsg_RemoveNamedObject,
                        OnRemoveNamedObject)
    IPC_MESSAGE_HANDLER(OhGinJavascriptBridgeMsg_DoCallH5Function,
                        OnDoCallH5Function)
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()
  return handled;
}

void OhGinJavascriptBridgeDispatcher::DidClearWindowObject() {
  if (inside_did_clear_window_object_)
    return;
  base::AutoReset<bool> flag_entry(&inside_did_clear_window_object_, true);
  for (NamedObjectMap::const_iterator iter = named_objects_.begin();
       iter != named_objects_.end(); ++iter) {
    if (objects_.Lookup(iter->second))
      objects_.Remove(iter->second);

    OhGinJavascriptBridgeObject* object =
        OhGinJavascriptBridgeObject::InjectNamed(render_frame()->GetWebFrame(),
                                                 AsWeakPtr(), iter->first,
                                                 iter->second);
    if (object) {
      objects_.AddWithID(object, iter->second);
    } else {
      LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::DidClearWindowObject "
                    "ObjectWrapperDeleted called";
      // Inform the host about wrapper creation failure.
      render_frame()->Send(
          new OhGinJavascriptBridgeHostMsg_ObjectWrapperDeleted(routing_id(),
                                                                iter->second));
    }
  }
}

void OhGinJavascriptBridgeDispatcher::OnAddNamedObject(const std::string& name,
                                                       ObjectID object_id) {
  named_objects_.insert(std::make_pair(name, object_id));
}

void OhGinJavascriptBridgeDispatcher::OnRemoveNamedObject(
    const std::string& name) {
  // Removal becomes in effect on next reload. We simply removing the entry
  // from the map here.
  NamedObjectMap::iterator iter = named_objects_.find(name);
  if (iter == named_objects_.end()) {
    return;
  }
  named_objects_.erase(iter);
}

void OhGinJavascriptBridgeDispatcher::GetJavascriptMethods(
    ObjectID object_id,
    std::set<std::string>* methods) {
  render_frame()->Send(new OhGinJavascriptBridgeHostMsg_GetMethods(
      routing_id(), object_id, methods));
}

bool OhGinJavascriptBridgeDispatcher::HasJavascriptMethod(
    ObjectID object_id,
    const std::string& method_name) {
  bool result;
  render_frame()->Send(new OhGinJavascriptBridgeHostMsg_HasMethod(
      routing_id(), object_id, method_name, &result));
  return result;
}

std::unique_ptr<base::Value>
OhGinJavascriptBridgeDispatcher::InvokeJavascriptMethod(
    ObjectID object_id,
    const std::string& method_name,
    const base::ListValue& arguments,
    OhGinJavascriptBridgeError* error) {
  base::ListValue result_wrapper;
  render_frame()->Send(new OhGinJavascriptBridgeHostMsg_InvokeMethod(
      routing_id(), object_id, method_name, arguments, &result_wrapper, error));
  base::Value* result;
  if (result_wrapper.Get(0, &result)) {
    return std::unique_ptr<base::Value>(result->CreateDeepCopy().release());
  } else {
    return std::unique_ptr<base::Value>();
  }
}

OhGinJavascriptBridgeObject* OhGinJavascriptBridgeDispatcher::GetObject(
    const ObjectID object_id) {
  OhGinJavascriptBridgeObject* result = objects_.Lookup(object_id);
  if (!result) {
    result =
        OhGinJavascriptBridgeObject::InjectAnonymous(AsWeakPtr(), object_id);
    if (result)
      objects_.AddWithID(result, object_id);
  }
  return result;
}

void OhGinJavascriptBridgeDispatcher::OnOhGinJavascriptBridgeObjectDeleted(
    const OhGinJavascriptBridgeObject* object) {
  int object_id = object->object_id();
  LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::"
                "OnOhGinJavascriptBridgeObjectDeleted called, object_id = "
             << object_id;
  // Ignore cleaning up of old object wrappers.
  if (objects_.Lookup(object_id) != object)
    return;
  LOG(DEBUG)
      << "OhGinJavascriptBridgeDispatcher::"
         "OnOhGinJavascriptBridgeObjectDeleted called, object_id = "
      << object_id
      << " ,new OhGinJavascriptBridgeHostMsg_ObjectWrapperDeleted called";
  objects_.Remove(object_id);
  render_frame()->Send(new OhGinJavascriptBridgeHostMsg_ObjectWrapperDeleted(
      routing_id(), object_id));
}

void OhGinJavascriptBridgeDispatcher::OnDestruct() {
  delete this;
}

OhGinJavascriptBridgeDispatcher::H5ObjectID OhGinJavascriptBridgeDispatcher::AddH5Object(
    v8::Local<v8::Object>& value) {
  LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::AddH5Object called";
  v8::Isolate* isolate = blink::MainThreadIsolate();
  v8::HandleScope handle_scope(isolate);

  v8::Persistent<v8::Value>* persistent = new v8::Persistent<v8::Value>(isolate, value);

  H5ObjectID object_id;
  {
    base::AutoLock locker(h5_objects_lock_);
    object_id = next_h5_object_id_++;
    h5_object_map_[object_id] = persistent;
  }

  return object_id;
}

std::vector<std::string> OhGinJavascriptBridgeDispatcher::GetH5ObjectMethodNames(
    v8::Local<v8::Object> object,
    int h5_object_id) {
  v8::Isolate* isolate = blink::MainThreadIsolate();
  v8::HandleScope handle_scope(isolate);

  if(!render_frame() || !render_frame()->GetWebFrame()) {
    LOG(ERROR) << "OhGinJavascriptBridgeDispatcher::GetH5ObjectMethodNames frame null";
    return std::vector<std::string>();
  }

  v8::Local<v8::Context> context = render_frame()->GetWebFrame()->MainWorldScriptContext();

  if(context.IsEmpty()) {
    LOG(ERROR) << "OhGinJavascriptBridgeDispatcher::GetH5ObjectMethodNames context empty";
    return std::vector<std::string>();
  }

  v8::Context::Scope context_scope(context);
  v8::Local<v8::Array> keys;
  v8::Local<v8::Value> key, value;

  if(!object->GetPropertyNames(context,
                               v8::KeyCollectionMode::kIncludePrototypes,
                               v8::PropertyFilter::ALL_PROPERTIES,
                               v8::IndexFilter::kIncludeIndices)
            .ToLocal(&keys)) {
    return std::vector<std::string>();
  }
  LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::GetH5ObjectMethodNames called";
  for (uint32_t i = 0; i < keys->Length(); ++i) {
    if(!keys->Get(context, i).ToLocal(&key)) {
      LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::GetH5ObjectMethodNames key error";
      continue;
    }

    v8::TryCatch try_catch(isolate);
    v8::MaybeLocal<v8::Value> maybe_value =
        object->Get(context, key);
    if (try_catch.HasCaught() || !maybe_value.ToLocal(&value)) {
      LOG(WARNING) << "OhGinJavascriptBridgeDispatcher::GetH5ObjectMethodNames Getter property fail";
      continue;
    }

    if(!value->IsFunction()) {
      continue;
    }

    int len = key.As<v8::String>()->Utf8Length(isolate);
    if (len == 0) {
      LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::GetH5ObjectMethodNames key len error";
      continue;
    }

    char* buf = new char[len];
    key.As<v8::String>()->WriteUtf8(
          isolate, buf, len, nullptr,
          v8::String::REPLACE_INVALID_UTF8);
    std::string method_name(buf, len);
    LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::GetH5ObjectMethodNames method_name = " << method_name;
    H5ObjectMethodsMap_[h5_object_id].push_back(method_name);
    delete[] buf;
  }

  return H5ObjectMethodsMap_[h5_object_id];
}

bool OhGinJavascriptBridgeDispatcher::HasH5ObjectMethod(
    v8::Local<v8::Object> object) {
  v8::Isolate* isolate = blink::MainThreadIsolate();
  v8::HandleScope handle_scope(isolate);
  if(!render_frame() || !render_frame()->GetWebFrame()) {
    LOG(ERROR) << "OhGinJavascriptBridgeDispatcher::HasH5ObjectMethod frame null";
    return false;
  }

  v8::Local<v8::Context> context = render_frame()->GetWebFrame()->MainWorldScriptContext();
  if(context.IsEmpty()) {
    LOG(ERROR) << "OhGinJavascriptBridgeDispatcher::HasH5ObjectMethod context empty";
    return false;
  }
  v8::Context::Scope context_scope(context);

  v8::Local<v8::Array> keys;
  v8::Local<v8::Value> key, value;
  LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::HasH5ObjectMethod called";
  if(!object->GetOwnPropertyNames(context).ToLocal(&keys)) {
    LOG(ERROR) << "OhGinJavascriptBridgeDispatcher::HasH5ObjectMethod GetOwnPropertyNames fail";
    return false;
  }

  for (uint32_t i = 0; i < keys->Length(); ++i) {
    if(!keys->Get(context, i).ToLocal(&key)) {
      LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::GetH5ObjectMethodNames key error";
      continue;
    }

    if (!key->IsString() &&
        !key->IsNumber()) {
      continue;
    }

    v8::String::Utf8Value name_utf8(isolate, key);
    v8::TryCatch try_catch(isolate);
    v8::MaybeLocal<v8::Value> maybe_value =
        object->Get(context, key);
    if (try_catch.HasCaught() || !maybe_value.ToLocal(&value)) {
      LOG(WARNING) << "OhGinJavascriptBridgeDispatcher::HasH5ObjectMethod Getter for property "
                   << *name_utf8 << " threw an exception.";
      continue;
    }

    if(value->IsFunction()) {
      LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::HasH5ObjectMethod return true";
      return true;
    }
  }
  LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::HasH5ObjectMethod return false";
  return false;
}

bool OhGinJavascriptBridgeDispatcher::HasH5ObjectMethod(
    int h5_object_id,
    const std::string& method_name) {
  if (H5ObjectMethodsMap_.find(h5_object_id) == H5ObjectMethodsMap_.end()) {
    return false;
  }

  for (std::vector<std::string>::iterator iter =
            H5ObjectMethodsMap_[h5_object_id].begin();
       iter != H5ObjectMethodsMap_[h5_object_id].end(); ++iter) {
    if (*iter == method_name) {
      LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::HasH5ObjectMethod return true";
      return true;
    }
  }
  LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::HasH5ObjectMethod return false";
  return false;
}

void OhGinJavascriptBridgeDispatcher::OnDoCallAnonymousH5Function(
    int32_t h5_object_id,
    const base::ListValue& args) {
  std::map<H5ObjectID, v8::Persistent<v8::Value>*>::iterator
      iter;
  {
    base::AutoLock locker(h5_objects_lock_);
    iter = h5_object_map_.find(h5_object_id);
    if (iter == h5_object_map_.end() || !(iter->second)) {
      LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::OnDoCallAnonymousH5Function obj id not found";
      return;
    }
  }

  v8::Isolate* isolate = blink::MainThreadIsolate();
  v8::HandleScope handle_scope(isolate);

  if (!render_frame() || !render_frame()->GetWebFrame()) {
    LOG(ERROR) << "OhGinJavascriptBridgeDispatcher::OnDoCallAnonymousH5Function frame null";
    return;
  }

  v8::Local<v8::Context> context =
      render_frame()->GetWebFrame()->MainWorldScriptContext();
  if(context.IsEmpty()) {
    LOG(ERROR) << "OhGinJavascriptBridgeDispatcher::OnDoCallAnonymousH5Function context empty";
    return;
  }
  v8::Context::Scope context_scope(context);

  v8::Local<v8::Object> object = v8::Local<v8::Object>::New(
      isolate, *reinterpret_cast<v8::Persistent<v8::Object>*>(iter->second));

  v8::Local<v8::Value>* v8_args = nullptr;
  int size = args.GetList().size();
  if (size > 0) {
    LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::OnDoCallAnonymousH5Function args size = " << size;
    v8_args = new v8::Local<v8::Value>[size];
  }

  for (int i = 0; i < size; ++i) {
    const base::Value& base_arg =
        args.GetList()[i];
    if (!base_arg.is_blob()) {
      LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::OnDoCallAnonymousH5Function not blob";
      v8_args[i] = converter_->ToV8Value(&base_arg, context);
      continue;
    }

    std::unique_ptr<const OhGinJavascriptBridgeValue> gin_value =
        OhGinJavascriptBridgeValue::FromValue(&base_arg);
    if (gin_value->IsType(OhGinJavascriptBridgeValue::TYPE_OBJECT_ID)) {
      OhGinJavascriptBridgeObject* object_result = NULL;
      OhGinJavascriptBridgeDispatcher::ObjectID object_id;
      if (gin_value->GetAsObjectID(&object_id)) {
        LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::OnDoCallAnonymousH5Function blob";
        object_result = GetObject(object_id);
      }
      if(object_result) {
        LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::OnDoCallAnonymousH5Function object_result ok";
        gin::Handle<OhGinJavascriptBridgeObject> controller =
            gin::CreateHandle(isolate, object_result);
        if(controller.IsEmpty()) {
          LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::OnDoCallAnonymousH5Function controller empty";
          v8_args[i] = v8::Undefined(isolate);
          continue;
        }
        v8_args[i] = controller.ToV8();
        continue;
      }
    } else if (gin_value->IsType(OhGinJavascriptBridgeValue::TYPE_NONFINITE)) {
      float float_value;
      gin_value->GetAsNonFinite(&float_value);
      v8_args[i] = v8::Number::New(isolate, float_value);
      continue;
    }
    v8_args[i] = v8::Undefined(isolate);
  }

  v8::Local<v8::Function> func = v8::Local<v8::Function>::Cast(object);
  v8::MaybeLocal<v8::Value> function_call_result =
      func->Call(context, context->Global(), size, v8_args);
  LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::OnDoCallAnonymousH5Function func call end";
  if (!function_call_result.IsEmpty()) {
    function_call_result.ToLocalChecked();
  }

  if (v8_args != nullptr) {
    delete[] v8_args;
  }
}

void OhGinJavascriptBridgeDispatcher::convertToV8Value(const base::ListValue& base_value, v8::Local<v8::Value>*& v8_value) {
  v8::Isolate* isolate = blink::MainThreadIsolate();
  v8::HandleScope handle_scope(isolate);

  if (!v8_value || !render_frame() || !render_frame()->GetWebFrame()) {
    return;
  }

  v8::Local<v8::Context> context =
      render_frame()->GetWebFrame()->MainWorldScriptContext();
  if(context.IsEmpty()) {
    return;
  }
  v8::Context::Scope context_scope(context);

  int size = base_value.GetList().size();
  for (int i = 0; i < size; ++i) {
    const base::Value& base_arg =
        base_value.GetList()[i];
    if (!base_arg.is_blob()) {
      v8_value[i] = converter_->ToV8Value(&base_arg, context);
      continue;
    }

    std::unique_ptr<const OhGinJavascriptBridgeValue> gin_value =
        OhGinJavascriptBridgeValue::FromValue(&base_arg);
    if (gin_value->IsType(OhGinJavascriptBridgeValue::TYPE_OBJECT_ID)) {
      OhGinJavascriptBridgeObject* object_result = NULL;
      OhGinJavascriptBridgeDispatcher::ObjectID object_id;
      if (gin_value->GetAsObjectID(&object_id)) {
        object_result = GetObject(object_id);
      }
      if(object_result) {
        gin::Handle<OhGinJavascriptBridgeObject> controller =
            gin::CreateHandle(isolate, object_result);
        if(controller.IsEmpty()) {
          v8_value[i] = v8::Undefined(isolate);
          continue;
        }
        v8_value[i] = controller.ToV8();
        continue;
      }
    } else if (gin_value->IsType(OhGinJavascriptBridgeValue::TYPE_NONFINITE)) {
      float float_value;
      gin_value->GetAsNonFinite(&float_value);
      v8_value[i] = v8::Number::New(isolate, float_value);
      continue;
    }
    v8_value[i] = v8::Undefined(isolate);
  }
}

void OhGinJavascriptBridgeDispatcher::OnDoCallH5Function(
    int32_t frame_routing_id,
    int32_t h5_object_id,
    const std::string& method_name,
    const base::ListValue& args) {
  if (frame_routing_id != routing_id()) {
    return;
  }

  std::map<H5ObjectID, v8::Persistent<v8::Value>*>::iterator
      iter;
  {
    base::AutoLock locker(h5_objects_lock_);
    iter = h5_object_map_.find(h5_object_id);
    if (iter == h5_object_map_.end() || !(iter->second)) {
      LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::OnDoCallH5Function obj id not found";
      return;
    }
  }

  if (method_name.empty()) {   // IF h5_method_name empty, call anonymous function
    OnDoCallAnonymousH5Function(h5_object_id, args);
  }

  v8::Isolate* isolate = blink::MainThreadIsolate();
  v8::HandleScope handle_scope(isolate);

  if (!render_frame() || !render_frame()->GetWebFrame()) {
    LOG(ERROR) << "OhGinJavascriptBridgeDispatcher::OnDoCallH5Function frame null";
    return;
  }

  v8::Local<v8::Context> context =
      render_frame()->GetWebFrame()->MainWorldScriptContext();

  if(context.IsEmpty()) {
    LOG(ERROR) << "OhGinJavascriptBridgeDispatcher::OnDoCallH5Function context empty";
    return;
  }
  v8::Context::Scope context_scope(context);

  if (!HasH5ObjectMethod(h5_object_id, method_name)) {
    return;
  }


  v8::Local<v8::Object> object = v8::Local<v8::Object>::New(
      isolate, *reinterpret_cast<v8::Persistent<v8::Object>*>(iter->second));
  v8::Local<v8::Value> key =
      v8::String::NewFromUtf8(isolate,
                              method_name.c_str())
          .ToLocalChecked();

  v8::Local<v8::Value>* v8_args = nullptr;
  int size = args.GetList().size();
  if (size > 0) {
    LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::OnDoCallH5Function args size = " << size;
    v8_args = new v8::Local<v8::Value>[size];
  }

  for (int i = 0; i < size; ++i) {
    const base::Value& base_arg =
        args.GetList()[i];
    if (!base_arg.is_blob()) {
      LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::OnDoCallH5Function not blob";
      v8_args[i] = converter_->ToV8Value(&base_arg, context);
      continue;
    }

    std::unique_ptr<const OhGinJavascriptBridgeValue> gin_value =
        OhGinJavascriptBridgeValue::FromValue(&base_arg);
    if (gin_value->IsType(OhGinJavascriptBridgeValue::TYPE_OBJECT_ID)) {
      OhGinJavascriptBridgeObject* object_result = NULL;
      OhGinJavascriptBridgeDispatcher::ObjectID object_id;
      if (gin_value->GetAsObjectID(&object_id)) {
        LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::OnDoCallH5Function blob";
        object_result = GetObject(object_id);
      }
      if(object_result) {
        LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::OnDoCallH5Function object_result ok";
        gin::Handle<OhGinJavascriptBridgeObject> controller =
            gin::CreateHandle(isolate, object_result);
        if(controller.IsEmpty()) {
          LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::OnDoCallH5Function controller empty";
          v8_args[i] = v8::Undefined(isolate);
          continue;
        }
        v8_args[i] = controller.ToV8();
        continue;
      }
    } else if (gin_value->IsType(OhGinJavascriptBridgeValue::TYPE_NONFINITE)) {
      float float_value;
      gin_value->GetAsNonFinite(&float_value);
      v8_args[i] = v8::Number::New(isolate, float_value);
      continue;
    }
    v8_args[i] = v8::Undefined(isolate);
  }

  v8::TryCatch try_catch(isolate);
  v8::MaybeLocal<v8::Value> result_value =
      object->Get(context, key);

  if (!try_catch.HasCaught() && !result_value.IsEmpty()) {
    v8::Local<v8::Value> value = result_value.ToLocalChecked();
    v8::Local<v8::Function> func = v8::Local<v8::Function>::Cast(value);
    v8::MaybeLocal<v8::Value> function_call_result =
        func->Call(context, object, size, v8_args);
    LOG(DEBUG) << "OhGinJavascriptBridgeDispatcher::OnDoCallH5Function func call end";
    if (!function_call_result.IsEmpty()) {
      function_call_result.ToLocalChecked();
    }
  }

  if (v8_args != nullptr) {
    delete[] v8_args;
  }
}

}  // namespace NWEB
